package com.google.bitcoin.db;


import com.google.bitcoin.core.*;

import java.util.List;
import java.util.LinkedList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import java.math.BigInteger;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;


public abstract class TransactionDB
{
    private static final Logger log = LoggerFactory.getLogger(TransactionDB.class);

    protected ThreadPoolExecutor executor;

    public TransactionDB()
    {   
        executor = new ThreadPoolExecutor(128,128,10, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>(20480), new DBThreadFactory());


    }

    public void receiveFromBlock(Transaction tx, StoredBlock block, BlockChain.NewBlockType blockType) 
        throws VerificationException, ScriptException
    {
        tx.addBlockAppearance(block.getHeader().getHash());

        try
        {

            saveTx(tx, true);
        }
        catch(DoubleSpendException e)
        {
            throw new RuntimeException(e);
        }
    }

    public abstract Transaction getTransaction(Sha256Hash hash);
    public abstract Transaction getTransaction(Sha256Hash hash, int connected_out_levels);

    public Collection<TransactionOutput> getUnspentOutputs(Collection<Address> addresses)
    {
        final LinkedList<TransactionOutput> out = new LinkedList<TransactionOutput>();
        final Semaphore wait_sem=new Semaphore(0);
        int count = addresses.size();

        for(final Address addr: addresses)
        {
            executor.execute(new Runnable()
            {
                public void run()
                {
                    while(true)
                    {
                        try
                        {
                            Collection<TransactionOutput> lst = getUnspentOutputs(addr);
                            synchronized(out)
                            {
                                out.addAll(lst);
                            }
                            wait_sem.release(1);
                            return;
                        }
                        catch(Throwable e)
                        {
                            e.printStackTrace();

                        }
                    }
                }

            });
        }

        wait_sem.acquireUninterruptibly(count);
        return out;
       
    }
    public abstract Collection<TransactionOutput> getUnspentOutputs(Address addr);
    public abstract Collection<TransactionOutput> getUnspentOutputs(Address addr, BigInteger min_value);

    public abstract void saveTx(Transaction tx, boolean allow_double_spend) throws VerificationException, DoubleSpendException;
    public abstract void removeTx(Transaction tx) throws com.google.bitcoin.core.ScriptException;
    public abstract Set<Transaction> getTransactions(boolean includeDead, boolean includeInactive);


    public void checkConsistency(){}

    public BigInteger getBalance(BalanceType balanceType, HashSet<Address> addresses)
    {
        return addUpOutputs(getUnspentOutputs(addresses), balanceType);
    }
    public BigInteger getBalance(BalanceType balanceType, Address addr)
    {
        return addUpOutputs(getUnspentOutputs(addr), balanceType);
    }
    protected BigInteger addUpOutputs(Collection<TransactionOutput> out_lst, BalanceType balanceType)
    {
        BigInteger value=BigInteger.ZERO;
        for(TransactionOutput out : out_lst)
        {
            Transaction tx= out.getParent();
            if (balanceType==BalanceType.AVAILABLE)
            {
                if (tx.getConfidence().getConfidenceType()==TransactionConfidence.ConfidenceType.BUILDING)
                {
                    log.debug("Adding value " + tx.getHash() + " " + out.getValue());
                    value=value.add(out.getValue());
                }
            }
            else if (balanceType==BalanceType.ESTIMATED)
            {
                log.debug("Adding value " + tx.getHash() + " " + out.getValue());
                value=value.add(out.getValue());
            }
        }
        return value;
 
    }

    public class DBThreadFactory implements ThreadFactory
    {

        public Thread newThread(Runnable r)
        {
            Thread t = new Thread(r);
            t.setDaemon(true);
            t.setName("DBThread");
            return t;
        }
    }

    public void shutdownExec()
        throws InterruptedException
    {
        executor.shutdown();
        executor.awaitTermination(2, TimeUnit.DAYS);

    }

    public void clearCache(){}
}


